Ознайомтеся з основами двійкових дерев пошуку (BST) та навчіться їх ефективно реалізовувати на JavaScript. Цей посібник охоплює структуру BST, операції та практичні приклади для розробників з усього світу.
Двійкові дерева пошуку: Повний посібник з реалізації на JavaScript
Двійкові дерева пошуку (BST) є фундаментальною структурою даних у комп'ютерних науках, що широко використовується для ефективного пошуку, сортування та вилучення даних. Їхня ієрархічна структура дозволяє досягти логарифмічної часової складності для багатьох операцій, що робить їх потужним інструментом для керування великими наборами даних. Цей посібник надає вичерпний огляд BST та демонструє їх реалізацію на JavaScript для розробників з усього світу.
Розуміння двійкових дерев пошуку
Що таке двійкове дерево пошуку?
Двійкове дерево пошуку — це деревоподібна структура даних, де кожен вузол має не більше двох нащадків, які називаються лівим та правим нащадком. Ключова властивість BST полягає в тому, що для будь-якого вузла:
- Усі вузли в лівому піддереві мають ключі, менші за ключ вузла.
- Усі вузли в правому піддереві мають ключі, більші за ключ вузла.
Ця властивість гарантує, що елементи в BST завжди впорядковані, що забезпечує ефективний пошук та вилучення даних.
Ключові поняття
- Вузол: Базова одиниця в дереві, що містить ключ (дані) та вказівники на лівого та правого нащадків.
- Корінь: Найвищий вузол у дереві.
- Листок: Вузол без нащадків.
- Піддерево: Частина дерева, що починається з певного вузла.
- Висота: Довжина найдовшого шляху від кореня до листка.
- Глибина: Довжина шляху від кореня до певного вузла.
Реалізація двійкового дерева пошуку на JavaScript
Визначення класу Node
Спочатку ми визначаємо клас `Node` для представлення кожного вузла в BST. Кожен вузол міститиме `key` для зберігання даних та вказівники `left` і `right` на своїх нащадків.
class Node {
constructor(key) {
this.key = key;
this.left = null;
this.right = null;
}
}
Визначення класу BinarySearchTree
Далі ми визначаємо клас `BinarySearchTree`. Цей клас міститиме кореневий вузол та методи для вставки, пошуку, видалення та обходу дерева.
class BinarySearchTree {
constructor() {
this.root = null;
}
// Методи будуть додані тут
}
Вставка
Метод `insert` додає новий вузол із заданим ключем до BST. Процес вставки підтримує властивість BST, розміщуючи новий вузол у відповідній позиції відносно існуючих вузлів.
insert(key) {
const newNode = new Node(key);
if (this.root === null) {
this.root = newNode;
} else {
this.insertNode(this.root, newNode);
}
}
insertNode(node, newNode) {
if (newNode.key < node.key) {
if (node.left === null) {
node.left = newNode;
} else {
this.insertNode(node.left, newNode);
}
} else {
if (node.right === null) {
node.right = newNode;
} else {
this.insertNode(node.right, newNode);
}
}
}
Приклад: Вставка значень у BST
const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
Пошук
Метод `search` перевіряє, чи існує вузол із заданим ключем у BST. Він проходить по дереву, порівнюючи ключ з ключем поточного вузла і переходячи до лівого або правого піддерева відповідно.
search(key) {
return this.searchNode(this.root, key);
}
searchNode(node, key) {
if (node === null) {
return false;
}
if (key < node.key) {
return this.searchNode(node.left, key);
} else if (key > node.key) {
return this.searchNode(node.right, key);
} else {
return true;
}
}
Приклад: Пошук значення в BST
console.log(bst.search(9)); // Вивід: true
console.log(bst.search(2)); // Вивід: false
Видалення
Метод `remove` видаляє вузол із заданим ключем з BST. Це найскладніша операція, оскільки потрібно підтримувати властивість BST під час видалення вузла. Існує три випадки, які слід розглянути:
- Випадок 1: Вузол, що видаляється, є листком. Просто видаліть його.
- Випадок 2: Вузол, що видаляється, має одного нащадка. Замініть вузол його нащадком.
- Випадок 3: Вузол, що видаляється, має двох нащадків. Знайдіть наступника в симетричному обході (найменший вузол у правому піддереві), замініть вузол цим наступником, а потім видаліть наступника.
remove(key) {
this.root = this.removeNode(this.root, key);
}
removeNode(node, key) {
if (node === null) {
return null;
}
if (key < node.key) {
node.left = this.removeNode(node.left, key);
return node;
} else if (key > node.key) {
node.right = this.removeNode(node.right, key);
return node;
} else {
// ключ дорівнює ключу вузла
// випадок 1 - вузол є листком
if (node.left === null && node.right === null) {
node = null;
return node;
}
// випадок 2 - вузол має лише одного нащадка
if (node.left === null) {
node = node.right;
return node;
} else if (node.right === null) {
node = node.left;
return node;
}
// випадок 3 - вузол має двох нащадків
const aux = this.findMinNode(node.right);
node.key = aux.key;
node.right = this.removeNode(node.right, aux.key);
return node;
}
}
findMinNode(node) {
let current = node;
while (current != null && current.left != null) {
current = current.left;
}
return current;
}
Приклад: Видалення значення з BST
bst.remove(7);
console.log(bst.search(7)); // Вивід: false
Обхід дерева
Обхід дерева полягає у відвідуванні кожного вузла в дереві в певному порядку. Існує кілька поширених методів обходу:
- Симетричний обхід (In-order): Відвідує ліве піддерево, потім вузол, потім праве піддерево. Це призводить до відвідування вузлів у порядку зростання.
- Прямий обхід (Pre-order): Відвідує вузол, потім ліве піддерево, потім праве піддерево.
- Зворотний обхід (Post-order): Відвідує ліве піддерево, потім праве піддерево, потім вузол.
inOrderTraverse(callback) {
this.inOrderTraverseNode(this.root, callback);
}
inOrderTraverseNode(node, callback) {
if (node !== null) {
this.inOrderTraverseNode(node.left, callback);
callback(node.key);
this.inOrderTraverseNode(node.right, callback);
}
}
preOrderTraverse(callback) {
this.preOrderTraverseNode(this.root, callback);
}
preOrderTraverseNode(node, callback) {
if (node !== null) {
callback(node.key);
this.preOrderTraverseNode(node.left, callback);
this.preOrderTraverseNode(node.right, callback);
}
}
postOrderTraverse(callback) {
this.postOrderTraverseNode(this.root, callback);
}
postOrderTraverseNode(node, callback) {
if (node !== null) {
this.postOrderTraverseNode(node.left, callback);
this.postOrderTraverseNode(node.right, callback);
callback(node.key);
}
}
Приклад: Обхід BST
const printNode = (value) => console.log(value);
bst.inOrderTraverse(printNode); // Вивід: 3 5 8 9 10 11 12 13 14 15 18 20 25
bst.preOrderTraverse(printNode); // Вивід: 11 5 3 9 8 10 15 13 12 14 20 18 25
bst.postOrderTraverse(printNode); // Вивід: 3 8 10 9 12 14 13 18 25 20 15 11
Мінімальне та максимальне значення
Знаходження мінімального та максимального значень у BST є простим завданням завдяки його впорядкованій природі.
min() {
return this.minNode(this.root);
}
minNode(node) {
let current = node;
while (current !== null && current.left !== null) {
current = current.left;
}
return current;
}
max() {
return this.maxNode(this.root);
}
maxNode(node) {
let current = node;
while (current !== null && current.right !== null) {
current = current.right;
}
return current;
}
Приклад: Пошук мінімального та максимального значень
console.log(bst.min().key); // Вивід: 3
console.log(bst.max().key); // Вивід: 25
Практичне застосування двійкових дерев пошуку
Двійкові дерева пошуку використовуються в різноманітних застосуваннях, зокрема:
- Бази даних: Індексація та пошук даних. Наприклад, багато систем баз даних використовують варіації BST, такі як B-дерева, для ефективного знаходження записів. Уявіть глобальний масштаб баз даних, що використовуються транснаціональними корпораціями; ефективне вилучення даних є першочерговим.
- Компілятори: Таблиці символів, що зберігають інформацію про змінні та функції.
- Операційні системи: Планування процесів та управління пам'яттю.
- Пошукові системи: Індексація веб-сторінок та ранжування результатів пошуку.
- Файлові системи: Організація та доступ до файлів. Уявіть файлову систему на сервері, що використовується для хостингу веб-сайтів по всьому світу; добре організована структура на основі BST допомагає швидко надавати контент.
Аспекти продуктивності
Продуктивність BST залежить від його структури. У найкращому випадку збалансоване BST дозволяє досягти логарифмічної часової складності для операцій вставки, пошуку та видалення. Однак у найгіршому випадку (наприклад, перекошене дерево) часова складність може погіршитися до лінійної.
Збалансовані та незбалансовані дерева
Збалансоване BST — це дерево, в якому висоти лівого та правого піддерев кожного вузла відрізняються не більше ніж на одиницю. Алгоритми самобалансування, такі як AVL-дерева та червоно-чорні дерева, гарантують, що дерево залишається збалансованим, забезпечуючи стабільну продуктивність. Різні регіони можуть вимагати різних рівнів оптимізації залежно від навантаження на сервер; балансування допомагає підтримувати продуктивність при високому глобальному використанні.
Часова складність
- Вставка: O(log n) в середньому, O(n) у найгіршому випадку.
- Пошук: O(log n) в середньому, O(n) у найгіршому випадку.
- Видалення: O(log n) в середньому, O(n) у найгіршому випадку.
- Обхід: O(n), де n — кількість вузлів у дереві.
Просунуті концепції BST
Самобалансуючі дерева
Самобалансуючі дерева — це BST, які автоматично коригують свою структуру для підтримки балансу. Це гарантує, що висота дерева залишається логарифмічною, забезпечуючи стабільну продуктивність для всіх операцій. До поширених самобалансуючих дерев належать AVL-дерева та червоно-чорні дерева.
AVL-дерева
AVL-дерева підтримують баланс, гарантуючи, що різниця висот між лівим та правим піддеревами будь-якого вузла становить не більше одиниці. Коли цей баланс порушується, виконуються обертання для його відновлення.
Червоно-чорні дерева
Червоно-чорні дерева використовують властивості кольору (червоний або чорний) для підтримки балансу. Вони складніші за AVL-дерева, але пропонують кращу продуктивність у певних сценаріях.
Приклад коду на JavaScript: Повна реалізація двійкового дерева пошуку
class Node {
constructor(key) {
this.key = key;
this.left = null;
this.right = null;
}
}
class BinarySearchTree {
constructor() {
this.root = null;
}
insert(key) {
const newNode = new Node(key);
if (this.root === null) {
this.root = newNode;
} else {
this.insertNode(this.root, newNode);
}
}
insertNode(node, newNode) {
if (newNode.key < node.key) {
if (node.left === null) {
node.left = newNode;
} else {
this.insertNode(node.left, newNode);
}
} else {
if (node.right === null) {
node.right = newNode;
} else {
this.insertNode(node.right, newNode);
}
}
}
search(key) {
return this.searchNode(this.root, key);
}
searchNode(node, key) {
if (node === null) {
return false;
}
if (key < node.key) {
return this.searchNode(node.left, key);
} else if (key > node.key) {
return this.searchNode(node.right, key);
} else {
return true;
}
}
remove(key) {
this.root = this.removeNode(this.root, key);
}
removeNode(node, key) {
if (node === null) {
return null;
}
if (key < node.key) {
node.left = this.removeNode(node.left, key);
return node;
} else if (key > node.key) {
node.right = this.removeNode(node.right, key);
return node;
} else {
// ключ дорівнює ключу вузла
// випадок 1 - вузол є листком
if (node.left === null && node.right === null) {
node = null;
return node;
}
// випадок 2 - вузол має лише одного нащадка
if (node.left === null) {
node = node.right;
return node;
} else if (node.right === null) {
node = node.left;
return node;
}
// випадок 3 - вузол має двох нащадків
const aux = this.findMinNode(node.right);
node.key = aux.key;
node.right = this.removeNode(node.right, aux.key);
return node;
}
}
findMinNode(node) {
let current = node;
while (current != null && current.left != null) {
current = current.left;
}
return current;
}
min() {
return this.minNode(this.root);
}
minNode(node) {
let current = node;
while (current !== null && current.left !== null) {
current = current.left;
}
return current;
}
max() {
return this.maxNode(this.root);
}
maxNode(node) {
let current = node;
while (current !== null && current.right !== null) {
current = current.right;
}
return current;
}
inOrderTraverse(callback) {
this.inOrderTraverseNode(this.root, callback);
}
inOrderTraverseNode(node, callback) {
if (node !== null) {
this.inOrderTraverseNode(node.left, callback);
callback(node.key);
this.inOrderTraverseNode(node.right, callback);
}
}
preOrderTraverse(callback) {
this.preOrderTraverseNode(this.root, callback);
}
preOrderTraverseNode(node, callback) {
if (node !== null) {
callback(node.key);
this.preOrderTraverseNode(node.left, callback);
this.preOrderTraverseNode(node.right, callback);
}
}
postOrderTraverse(callback) {
this.postOrderTraverseNode(this.root, callback);
}
postOrderTraverseNode(node, callback) {
if (node !== null) {
this.postOrderTraverseNode(node.left, callback);
this.postOrderTraverseNode(node.right, callback);
callback(node.key);
}
}
}
// Приклад використання
const bst = new BinarySearchTree();
bst.insert(11);
bst.insert(7);
bst.insert(15);
bst.insert(5);
bst.insert(3);
bst.insert(9);
bst.insert(8);
bst.insert(10);
bst.insert(13);
bst.insert(12);
bst.insert(14);
bst.insert(20);
bst.insert(18);
bst.insert(25);
const printNode = (value) => console.log(value);
console.log("Симетричний обхід (in-order):");
bst.inOrderTraverse(printNode);
console.log("Прямий обхід (pre-order):");
bst.preOrderTraverse(printNode);
console.log("Зворотний обхід (post-order):");
bst.postOrderTraverse(printNode);
console.log("Мінімальне значення:", bst.min().key);
console.log("Максимальне значення:", bst.max().key);
console.log("Пошук 9:", bst.search(9));
console.log("Пошук 2:", bst.search(2));
bst.remove(7);
console.log("Пошук 7 після видалення:", bst.search(7));
Висновок
Двійкові дерева пошуку є потужною та універсальною структурою даних з численними застосуваннями. Цей посібник надав вичерпний огляд BST, охоплюючи їх структуру, операції та реалізацію на JavaScript. Розуміючи принципи та техніки, обговорені в цьому посібнику, розробники з усього світу можуть ефективно використовувати BST для вирішення широкого кола проблем у розробці програмного забезпечення. Від керування глобальними базами даних до оптимізації алгоритмів пошуку, знання BST є безцінним активом для будь-якого програміста.
Продовжуючи свою подорож у комп'ютерних науках, дослідження просунутих концепцій, таких як самобалансуючі дерева та їхні різноманітні реалізації, ще більше поглибить ваше розуміння та можливості. Продовжуйте практикуватися та експериментувати з різними сценаріями, щоб оволодіти мистецтвом ефективного використання двійкових дерев пошуку.